/* This file is part of SpaceSim.
 
 SpaceSim is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
 
 SpaceSim is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with SpaceSim.  If not, see <http://www.gnu.org/licenses/>. */

#include <cmath>
#include <sstream>

#include "player.h"

#include "main.h"
#include "planet.h"
#include "time.h"

template <class T>
inline std::string toString (const T& t) {
	std::stringstream str;
	str << t;
	return str.str();
}

Player player;

void Player::Update() {
	if (XPos != XPos || YPos != YPos) {	// nans always return false so NOT A BUG!!!
		// something broke really bad
		Graphics::ViewStack.push(Graphics::ViewMode::ErrorView);
		Graphics::ErrorString.SetText("Error: Invalid location.");
		XPos = 100.0;
		YPos = 0.0;
		Rotation = 0.0;
		XSpeed = 0.0;
		YSpeed = 0.0;
		RSpeed = 0.0;
	}
	DockTarget = Main::CurrentCluster.GetSystem(Main::SubSystem)->GetPlanetPtr(TargetPlanet);
	if (ControlMode == ShipControl::Manual) {
		RTToXY(); // our data is generally in rotated coordinates, change it to (X,Y)
		
		XPos -= XSpeed * Time::UpdateTime;
		YPos -= YSpeed * Time::UpdateTime;
		Rotation += RSpeed * Time::UpdateTime;
	} else if (ControlMode == ShipControl::Orbit) {
		ROtoXY();
		XPos = OXPos;
		YPos = OYPos;
	} else if (ControlMode == ShipControl::Dock) {	// This might be a bit buggy
		if (DockTarget == NULL || DockTarget == 0) {
			Main::Error("Invalid planet selected!");
		}
		
		float TargetRadius = DockTarget->GetOrbitRadius();
		
		// if we are ahead of the docktarget, move into a lower orbit to slow down
		float TargetFraction = fabs(DockTarget->GetOrbitFraction());	// get the orbit part we want
		
		float OrbitFraction = fabs(GetOrbitFraction());	// get our own orbit part
		
		if (TargetFraction == 0) {	// weed out divide by 0 errors
			TargetFraction = 0.001;
		}
		if (OrbitFraction == 0) {
			OrbitFraction = 0.001;
		}
		
		float Difference = (OrbitFraction - TargetFraction); // if we are ahead, it's big, and high orbits go slower
		
		while (Difference > 1.0) {	// Difference should be between +1.0 and -1.0
			Difference -= 1.0;
		}
		while (Difference < -1.0) {
			Difference += 1.0;
		}
		
		// Now, we exaggerate difference by using this formula: a * cuberoot(x) + 1 = y
		// Change a for the farthest the player can get from a planet
		// Currently, it's 0.25, so the player can be 1/4th closer or farther
		Difference = 0.25 * ((Difference<0.0?-1.0:1.0) * pow(fabs(Difference), 1.0 / 3.0)) + 1.0;
	
		Main::Message.SetText("Synchronizing, difference: " + toString(Difference));
		
		float NewORadius = TargetRadius * Difference;
		
		if (NewORadius > ORadius + ActiveShip->GetAcceleration() * Time::UpdateTime) { // don't move too fast
			ORadius = ORadius + ActiveShip->GetAcceleration() * Time::UpdateTime;
		}
		if (NewORadius < ORadius - ActiveShip->GetAcceleration() * Time::UpdateTime) {
			ORadius = ORadius - ActiveShip->GetAcceleration() * Time::UpdateTime;
		}
		
		if (fabs(OrbitFraction - TargetFraction) < 0.05 && fabs(NewORadius - TargetRadius) < 10.0) {	// basically, if we are close enough
			if (DockTarget->GetType() == PlanetType::WarpLauncher) {
				Graphics::ViewStack.push(Graphics::ViewMode::WarpSelectView);
				SetOrbitControl();
			} else if (DockTarget->GetType() == PlanetType::HubLauncher) {
				Graphics::ViewStack.push(Graphics::ViewMode::WarpHubView);
				SetOrbitControl();
			} else {
				Main::Message.SetText("Welcome to the planet gravity well.");
				SetOrbitControl();
			}
		}
		
		if (ORadius != ORadius) {	// nan is always false in a comparison! This IS correct code. Seriously. It's intended to be x != x. Believe me.
			ORadius = TargetRadius;	// this means something very very baaad is happening
			OrbitFraction = 0;						// reset to something sensible
			Main::Message.SetText("Error: Wrong orbit in OrbitPlanner.");
		}
				
		RecalculateOrbitFraction(OrbitFraction);
		
		ROtoXY();
		XPos = OXPos;
		YPos = OYPos;
	}
	
	Sprite.SetPosition(XPos * Graphics::ZoomFactor, YPos * Graphics::ZoomFactor);
	Sprite.SetRotation(Rotation);
}

void Player::Accelerate() {
	if (ControlMode == ShipControl::Manual) {
		TSpeed += ActiveShip->GetAcceleration() * Time::UpdateTime / Graphics::ZoomFactor;
	} else if (ControlMode == ShipControl::Orbit) {
		ORadius += ActiveShip->GetAcceleration() * Time::UpdateTime;
		RecalculateOrbit();
	}
}

void Player::TurnLeft() {
	if (ControlMode == ShipControl::Manual)
		RSpeed += ActiveShip->GetTurnAccel() * Time::UpdateTime;
}

void Player::TurnRight() {
	if (ControlMode == ShipControl::Manual)
		RSpeed -= ActiveShip->GetTurnAccel() * Time::UpdateTime;
}

void Player::Reverse() {
	if (ControlMode == ShipControl::Manual) {
		RSpeed = (atan2(-XSpeed, -YSpeed) * 180 / PI - Rotation) * 10 * ActiveShip->GetTurnAccel() * Time::UpdateTime;
	} else if (ControlMode == ShipControl::Orbit) {
		ORadius -= ActiveShip->GetAcceleration() * Time::UpdateTime;
		RecalculateOrbit();
	}
}

void Player::Aim() {
	if (ControlMode == ShipControl::Manual) {
		Rotation = atan2(XPos - DockTarget->GetX(), YPos - DockTarget->GetY()) * 180 / PI;
		if (Rotation != Rotation) {
			Rotation = 0;
		}
	}	
}

void Player::SetOrbitControl() {
	if (ControlMode == ShipControl::Manual) {
		Main::Message.SetText("Reverting to orbit control.");
		ORadius = sqrt(XPos * XPos + YPos * YPos);	// calculate the orbit radius
		OXPos = XPos;	// set the location
		OYPos = YPos;
		RecalculateOrbit();	// time and starting point will have changed
	}
	ControlMode = ShipControl::Orbit;	// orbit control activated
}

void Player::SetManualControl() {
	if (ControlMode != ShipControl::Manual) {
		Main::Message.SetText("Manual control activated.");
		ControlMode = ShipControl::Manual;	// manual control activated
		XPos = -OXPos;	// set the location
		YPos = -OYPos;
		XSpeed = 0;	// brake the ship
		YSpeed = 0;
		RSpeed = 0;
	}
}

void Player::SetDockControl() {
	Main::Message.SetText("Aligning to selection.");
	if (ControlMode == ShipControl::Manual) { // if it's orbit or dock control, we already have ox, oy and or
		ORadius = sqrt(XPos * XPos + YPos * YPos);	// calculate the orbit radius
		OXPos = -XPos;	// set the location
		OYPos = -YPos;
		RecalculateOrbit();	// time and starting point will have changed
	}
	ControlMode = ShipControl::Dock;	// dock control activated
}

void Player::TryToDock() {
	float dx = DockTarget->GetX() - XPos;
	float dy = DockTarget->GetY() - YPos;
		
	if (dx * dx + dy * dy < 1000) {	// basically, if we are close enough
		if (DockTarget->GetType() == PlanetType::WarpLauncher) {
			Graphics::ViewStack.push(Graphics::ViewMode::WarpSelectView);
		} else if (DockTarget->GetType() == PlanetType::HubLauncher) {
			Graphics::ViewStack.push(Graphics::ViewMode::WarpHubView);
		} else {
			Graphics::ViewStack.push(Graphics::ViewMode::StationView);
			Main::Message.SetText("Welcome to the planet gravity well.");
			Main::SelectedPlanet = TargetPlanet;
			
			if (ActiveMission.IsDone()) {
				Main::Message.SetText("Mission done, reward: " + toString(ActiveMission.GetReward()));
				ActiveMission = Mission();
			}
		}
	}
}